Testing con Jasmine

Descripcion

Como realizar testing en Angular utilizando el framework the Jasmine

Se mostraran 3 tipos de tests:

Cobertura de código

Para ejecutar los test y que se nos muestre la cobertura de código tenemos que ejecutar los tests con el siguiente comando:

ng test --code-coverage
Metodo

La estructura base de un test es la siguiente:

 import { ComponentFixture, TestBed } from '@angular/core/testing';

import { Test1Component } from './test1.component';

describe('Test1Component', () => {
  let component: Test1Component;
  let fixture: ComponentFixture<Test1Component>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ Test1Component ]
    })
    .compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(Test1Component);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

Testear numero de etiquetas creadas

Para testear el numero de tags creados podemos hacerlo de la siguiente manera

it('should have a row per member', () => {
  component.membersList = membersListMock;
  fixture.detectChanges();
  const compiled = fixture.debugElement.queryAll(
    By.css('app-member-row')
  ).length;
  expect(compiled).toBe(4);
});

Utilizamos fixture.debugElement.queryAll para seleccionar las etiquetas que queremos contar

y dentro utilizamos el objeto By.css para indicar el selector css de la etiqeta.

El fixture.debugElement.queryAll devuelve un array con todas las etiquetas coincidentes con el selector, de manera que podemos contarlas con .length


Testear contenido de etiquetas

Para testear el contenido de una etiqueta lo hacemos de la siguiente manera:

it('should have a "Members List" header', () => {
  const compiled: HTMLElement = fixture.nativeElement.querySelector('h2');
  expect(compiled.textContent).toContain('Members List');
});

En este caso utilizamos fixture.nativeElement.querySelector para seleccionar una etiqueta en concreto, y utilizamos la propiedad textContent para leer el contenido de dicha etiqueta


Testear atributos de etiquetas

Si queremos testear el contenido de un atributo de una etiqueta lo podemos hacer de la siguiente manera:

Si tenemos una etiqueta como la siguiente:

<h2 ejemplo="valor">Ejemplo de cabecera</h2>

Podemos testear el contenido del atributo ejemplo de la siguiente manera:

it('should have atribute ejemplo with value valor', () => {
  const compiled: HTMLElement = fixture.nativeElement.querySelector('h2');
  expect(compiled.getAttribute("ejemplo")).toEqual('valor');
});

Si queremos testear si un atributo existe en una etiqueta o no, podemos testearlo contra null tal que así:

it('should have atribute ejemplo with value valor', () => {
  const compiled: HTMLElement = fixture.nativeElement.querySelector('h2');
  expect(compiled.getAttribute("ejemplo2")).toEqual(null);
});

Testear HttpClient

Codigo de Ejemplo

Para testear el HttpClient primero tenemos que realizar el siguiente import en el test:

imports: [HttpClientTestingModule]

Declaramos un objeto httpTestingController:

httpTestingController = TestBed.inject(HttpTestingController);

Ahora declaramos el test:

  it('check HttpClient', () => {
    const finalData = [{ userId: 1, id: 1, name: 'nombre', completed: true }];

    const req = httpTestingController.expectOne("http://127.0.0.1:8080/api/members");
    expect(req.request.method).toEqual('GET');
    req.flush(finalData);
    expect(component.messageObject).toEqual(finalData);
  });

Usamos el httpTestingController.expectOne para indicar que conexion queremos testear, en este caso en el servicio tenemos esto:

Por lo tanto en el expectOne tenemos que indicar la misma dirección

Despues usamos el flush para indicar los datos que recibira la "Solicictud mockeada", en este caso enviamos el objeto finalData y a continuación comprobamos que finalData sea igual a component.messageObject que es la variable del componente donde se almacena la solicitud que recibe.

Testear HttpClient en el servicio

Si estamos testeando el servicio directamente, tendremos que llamar a la función del servicio en el test, el test quedaría tal que así:

  it('check HttpClient', () => {
    const finalData = [{ userId: 1, id: 1, name: 'nombre', completed: true }];

    service
      .getInfo()
      .subscribe((data) => expect(data).toEqual(finalData));

    const req = httpTestingController.expectOne('http://127.0.0.1:8080/api/members');
    expect(req.request.method).toEqual('GET');
    req.flush(finalData);
  });

Testear Errores HttpClient

Para testear cuando se produce un error con el HttpClient lo hacemos de la siguiente manera:

  it('Check ERROR from HTTP server', async () => {
    let http = TestBed.inject(HttpTestingController);
    let errResponse: any;
    const mockErrorResponse = { status: 500, statusText: 'Server not available' };
    const data = 'An error occur on the server side';
    service.getInfo().subscribe({
      error: (e) => (errResponse = e)
  });
    http.expectOne("http://127.0.0.1:8080/api/members").flush(data, mockErrorResponse);
    expect(errResponse.message).toEqual("Error on request");
  });

Usamos mockErrorResponse para indicar el mensaje de error y el codigo de respuesta

const mockErrorResponse = { status: 500, statusText: 'Server not available' };

En realidad lo único importante es el codigo de error ya que el mensaje (En este caso concreto) no se utilizará para nada, ya que el error que se lanza es uno concreto puesto en el servicio

De igual manera data tampoco tiene ningun uso, ya que será el cuerpo de la respuesta, pero al ser un error, no lo usaremos para nada

Cuando llamamos al servicio y le asignamos un observable lo tenemos que hacer con el campo de error tal que asi:

service.getInfo().subscribe({
      error: (e) => (errResponse = e)
  });

El mensaje de error viene del servicio donde se define como se lanza el error:

Cuando se lanza el expect dentro indicamos errResponse.message ya que message es una de las propiedades del objeto Error que está lanzando el servicio, otra manera de compararlo seria la siguiente:

expect(errResponse).toEqual(new Error("Error on request"));

Testear Servicios privados

En el caso de que tengamos un servicio que lo tenemos declarado como privado podemos acceder a el con la siguiente sintaxis:

component['variablePrivada']

En el ejemplo anterior estamos almacenando el servicio, que es privado dentro componente, en una variable, esto nos permite utilizar un spyOn sobre el y así saber cuando se llama a la funcion showError del servicio.



Testear Routing

Para hacer tests relaccionados con rutas lo hacemos de la siguiente manera:

Primero tenemos que importar el RouterTestingModule:

imports: [RouterTestingModule.withRoutes(routes)]

el parametro routes hace referencia a la tabla de rutas que creamos mas adelante para el test

Creamos un objeto Router:

router = TestBed.inject(Router);

Creamos un objeto Location:

location = TestBed.inject(Location);

Despues tenemos que crear una tabla de rutas para el test:

const routes: Routes = [
    {path: 'home', component: MemberProfilePageComponent},
    {path: 'main', component: MemberProfilePageComponent},
  ];

el archivo del test quedaría tal que asi:

Y declaramos el test en si:

it('Test navigation going back when calling redirectToMainPage', fakeAsync (() => {
    component.isButtonEnabled = true;
    router.navigate(['/home']);
    tick();
    router.navigate(['/main']);
    tick();
    component.redirectToMainPage();
    tick();
    expect(location.path()).toBe('/home');
}));

En este caso estamos testeando la funcionalidad de redirectToMainPage que vuelve a la ruta anterior.

Para simular la navegacion en los tests tenemos que usar fakeAsync y la funcion tick() despues de cada accion de navegación

En este caso estamos navegando a home, despues a main y despues llamamos a redirectToMainPage y al hacer la compobación con location.path() la direccion tendría que ser home

NOTA: El import de Location tiene que ser exactamente el siguiente, sino no funciona (por lo visto hay varios tipos de Location):

import { Location } from '@angular/common';

Testear setTimeout

Para testear una funcion setTimeout tenemos que utilizar fakeAsync y tick para simular el paso del tiempo y que la funcion se dispare

import { fakeAsync, tick} from '@angular/core/testing';

En el test tenemos que pasarle un numero entero a tick para simular el tiempo que queremos que pase

Ejemplo:

Funcion:

Test:

si hay una funcion setTimeout que se ha llamado, tendremos que ejecutar tick con el tiempo necesario para que esa funcion timeout concluya su temporizador y se dispare.



Testear con spyOn

Para testear con el spyOn tenemos que indicar cual es el objeto y su metodo sobre el que poner el spy, una vez tenemos configurado el spy, solo tenemoq que llamar a expect para comprobar si se ha llamado a esa funcion o si se ha llamado con un parametro concreto.

Ejemplo de spyOn de un servicio:

Componente:

Test:

Ejemplo de spyOn de un EventEmitter:

Componente:

Test:

Tenemos que tener en cuenta de llamar a la funcion (en este caso ngOnInit) despues de hacer el spyOn, ya que el componente se crea antes de ejecutar el test y por lo tanto ngOnInit por defecto se llama antes del test (osea antes de aplicar el spyOn), asi que para que se vuelva a ejecutar la funcion (mostrarTexto o el emitter en este caso) sobre la que llamamos al spyOn tenemos que volver a ejecutar el ngOnInit.

Testear comoprtamientos Responsive

Para poder testear una aplicación responsive tenemos que instalar el paquete karma viewport

Paquete karma-iewport

Testear Excepciones

Para testear si una funciona lanza una excepción lo hacemos de la siguiente manera:

it('check checkValidForm to throw an exception', () => {
    expect( function(){ component.checkValidForm(); } ).toThrow(new Error("TEMPLATES.INVALID"));
});

Con ese código testearíamos cuando se lanza una excepción como la siguiente:

Mockear funciones y su return value

Si queremos mockear una funcion lo hacemos de la siguiente manera.

Codigo a testear:

Test:

  it('check myMessage is set', () => {
    spyOn(component, "setHello").and.returnValue("Hello World!")
    component.ngOnInit();

    expect(component.myMessage).toEqual('Hello World!');
  });

Para mockear una funcion simplemente usamos un spyOn e indicamos el valor a devolver con:

and.returnValue

Si queremos mockear una función que no devuelve nada (void) simplemente hacemos el spyOn con un returnValue vacio:

.and.returnValue()

Modificar fecha para los tests

Si queremos modificar la fecha para algun test que dependa de la hora actual podemos hacerlo asi:

let today = moment("2023-04-02").toDate();
jasmine.clock().mockDate(today);
Tags

Test | Jasmine | Angular